﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace 键盘钩子示例
{
    public partial class Form1 : Form
    {
        // 扫码枪模拟键盘输入时，会通过 Windows 的键盘消息传递数据。通过安装 低级键盘钩子(WH_KEYBOARD_LL),
        // 我们可以捕获所有键盘事件，而无需依赖应用程序是否有焦点。

        private const int WH_KEYBOARD_LL = 13; //WH_KEYBOARD_LL=13为低级键盘钩子的类型。
        private const int WM_KEYDOWN = 0x0100; // 0x0100,10进制是256,表示键盘按下事件的消息常量。
        private const int MAX_SCAN_BUFFER = 100;

        private IntPtr _hookID = IntPtr.Zero;
        private StringBuilder _scanBuffer = new StringBuilder();  // 用于拼接扫码枪输入的完整字符串。
        private DateTime _lastKeyPressTime = DateTime.MinValue;  // 记录上一次按键的时间，用于判断扫码枪的连续输入。

        [StructLayout(LayoutKind.Sequential)]
        private struct KBDLLHOOKSTRUCT
        {
            public uint vkCode;     // 虚拟键码（例如 A 的键码是 65）。
            public uint scanCode;   // 硬件扫描码。
            public uint flags;      // 标志位（例如是否是扩展键）。
            public uint time;       // 按键事件发生的时间戳。
            public IntPtr dwExtraInfo; // 额外信息（通常用于区分输入来源）。
        }

        private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
        /**
           * SetWindowsHookEx 其四个参数分别为钩子类型、钩子函数地址、钩子函数所在DLL的实例句柄、
           * 安装钩子后想监控的线程的ID号（设置ID为0，安装一个全局的钩子）。返回参数为钩子句柄。该方法的原型和参数解析如下：

              钩子类型：指定要安装的钩子类型，例如WH_KEYBOARD、WH_MOUSE等。
              钩子函数地址：指向钩子函数的指针，该函数将处理与钩子相关的事件。
              钩子函数所在DLL的实例句柄：如果钩子函数位于DLL中，则需要提供该DLL的实例句柄；如果是当前进程中的函数，则可以使用GetModuleHandle(NULL)获取句柄。
              安装钩子后想监控的线程的ID号：指定要监控的线程ID，若设置为0，则表示安装一个全局钩子，监控所有线程。
              该方法的原型大致如下：
                  HHOOK SetWindowsHookEx(
                    int       idHook,
                    HOOKPROC  lpfn,
                    HINSTANCE hMod,
                    DWORD     dwThreadId
               );
             返回值为钩子句柄，用于后续操作或卸载钩子时使用。
           * 
       */
        // 安装钩子
        // windows api(非托管):user32.dll
        // windows所有的UI和消息机制都在user32.dll中
        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc callback, IntPtr hMod, uint threadId);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool UnhookWindowsHookEx(IntPtr hhk);

        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr GetModuleHandle(string lpModuleName);

        [DllImport("user32.dll")]
        private static extern short GetAsyncKeyState(Keys vKey);

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            StartHook();
        }

        private void StartHook()
        {
            using (Process curProcess = Process.GetCurrentProcess())
            using (ProcessModule curModule = curProcess.MainModule)
            {
                // SetWindowsHookEx：安装钩子，传入钩子类型（WH_KEYBOARD_LL）、回调函数（HookCallback）、
                // 模块句柄（当前进程的模块）和线程 ID（0 表示全局）。
                _hookID = SetWindowsHookEx(WH_KEYBOARD_LL, HookCallback, GetModuleHandle(curModule.ModuleName), 0);
            }

            if (_hookID == IntPtr.Zero)
            {
                MessageBox.Show("无法安装钩子！", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        /// <summary>
        /// 卸载钩子，释放资源。
        /// </summary>
        private void StopHook()
        {
            if (_hookID != IntPtr.Zero)
            {
                UnhookWindowsHookEx(_hookID);
                _hookID = IntPtr.Zero;
            }
        }

        /// <summary>
        /// 钩子回调函数（HookCallback） 每当发生键盘事件时，Windows 会调用这个回调函数，我们在这里处理扫码枪的数据。
        /// </summary>
        /// <param name="nCode"></param>
        /// <param name="wParam"></param>
        /// <param name="lParam"></param>
        /// <returns></returns>
        private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
        {
            // wParam == WM_KEYDOWN：确保事件是按键按下。
            // nCode >= 0：确保钩子可以处理该事件（nCode < 0 时直接返回）。
            if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN)
            {
                // KBDLLHOOKSTRUCT：Windows 定义的结构体，包含按键的虚拟键码（vkCode）、扫描码等。
                KBDLLHOOKSTRUCT hookStruct = (KBDLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(KBDLLHOOKSTRUCT));
                // hookStruct.vkCode：将虚拟键码转换为字符（char）。
                char keyChar = (char)hookStruct.vkCode;

                // 区分扫码枪与手动输入
                // 时间间隔判断：扫码枪输入速度极快（通常连续按键间隔 < 100ms），而手动输入间隔较长。
                // 如果当前按键与上一次按键的时间差 < 100ms，则认为是扫码枪输入，将字符追加到缓冲区。
                // 否则认为是手动输入，清空缓冲区并重新开始。
                // 判断是否为扫码枪输入（时间间隔小于 100ms）
                if ((DateTime.Now - _lastKeyPressTime).TotalMilliseconds < 100)
                {
                    _scanBuffer.Append(keyChar);
                }
                else
                {
                    _scanBuffer.Clear();
                    _scanBuffer.Append(keyChar);
                }

                _lastKeyPressTime = DateTime.Now;

                // 结束符检测：扫码枪通常以回车键（Enter，ASCII 13 或 \r）结束输入。当检测到 \r 时，认为一串扫码数据完成。
                // 检测 Enter 键作为结束符
                if (keyChar == '\r')
                {
                    string result = _scanBuffer.ToString();
                    if (!string.IsNullOrEmpty(result))
                    {
                        // 跨线程更新 UI
                        this.Invoke((MethodInvoker)(() =>
                        {
                            txtResult.AppendText("扫码结果：" + result + Environment.NewLine);
                            txtResult.ScrollToCaret();
                        }));
                        _scanBuffer.Clear();
                    }
                }
            }

            // 将事件传递给下一个钩子（或系统默认处理），否则其他程序可能无法接收到按键事件。
            return CallNextHookEx(_hookID, nCode, wParam, lParam);
        }

        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            StopHook();
        }
    }
}
